package com.xuesinuo.ots.api;

import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.xuesinuo.ots.config.auth.Auth;
import com.xuesinuo.ots.config.i18n.I18nTips;
import com.xuesinuo.ots.config.pojo.PageParam;
import com.xuesinuo.ots.config.pojo.PageResult;
import com.xuesinuo.ots.config.spring.ApiCode;
import com.xuesinuo.ots.config.spring.ApiException;
import com.xuesinuo.ots.config.spring.ApiResultPacking;
import com.xuesinuo.ots.entity.Role;
import com.xuesinuo.ots.entity.User;
import com.xuesinuo.ots.entity.UserToken;
import com.xuesinuo.ots.pojo.RolePojo;
import com.xuesinuo.ots.tool.UserTool;
import com.xuesinuo.ots.util.BeanUtil;
import com.xuesinuo.ots.util.CaptchUtil;
import com.xuesinuo.ots.util.IdUtil;
import com.xuesinuo.ots.util.StringUtil;
import com.xuesinuo.pignoo.core.Pignoo;
import com.xuesinuo.xtool.Np;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;

/** 后台用户 */
@RestController
@RequestMapping("/backend/user")
@ApiResultPacking
@RequiredArgsConstructor
public class UserApi {
    private static final Cache<String, String> captchaCache = CacheBuilder.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();
    private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(12);
    private final UserTool userTool;
    private final Pignoo pignoo;

    @Data
    @AllArgsConstructor
    public static class CaptchaResult {
        /** 验证码ID */
        private String uid;
        /** 验证码的base64图。拼接到img标签需要前面加【data:image/png;base64,】 */
        private String base64;
    }

    /** 获取图片验证码 */
    @GetMapping("/captcha")
    public CaptchaResult captcha() {
        String uuid = IdUtil.getUUID();
        String code = StringUtil.random("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 4);
        byte[] img = CaptchUtil.pngCaptch(code);
        String base64 = Base64.getEncoder().encodeToString(img);
        captchaCache.put(uuid, code);
        return new CaptchaResult(uuid, base64);
    }

    @Data
    public static class LoginParam {
        @NotBlank
        private String username;
        @NotBlank
        private String password;
        @NotBlank
        private String captcha;
        @NotBlank
        private String captchaUid;
    }

    @Data
    @AllArgsConstructor
    public static class LoginResult {
        private String token;
    }

    /** 登录 */
    @Transactional
    @PostMapping("/login")
    public LoginResult login(@RequestBody @Validated LoginParam param) {
        String captcha = captchaCache.getIfPresent(param.getCaptchaUid());
        if (Np.i(captcha).x(x -> x.toLowerCase()).notEq(param.getCaptcha().toLowerCase())) {
            throw new ApiException(ApiCode.biz_error, I18nTips.captcha_error);
        }
        User user = pignoo.reader(User.class).filter(User::getUsername, "==", param.getUsername()).getAny();
        if (user.getHasUsing() != true) {
            throw new ApiException(ApiCode.biz_error, I18nTips.user_disabled);
        }
        boolean passwordOk = passwordEncoder.matches(param.getPassword(), user.getPassword());
        if (!passwordOk) {
            throw new ApiException(ApiCode.biz_error, I18nTips.username_password_error);
        }
        String token = IdUtil.getUUID();
        var userTokenWriter = pignoo.writer(UserToken.class);
        userTokenWriter.filter(UserToken::getUserId, "==", user.getId()).removeAll();
        UserToken userToken = new UserToken();
        userToken.setUserId(user.getId());
        userToken.setToken(token);
        userToken.setCreateDatetime(System.currentTimeMillis());
        userToken.setExpireDatetime(System.currentTimeMillis() + 2 * 24 * 60 * 60 * 1000);
        userTokenWriter.add(userToken);
        captchaCache.invalidate(param.getCaptchaUid());
        return new LoginResult(token);
    }

    /** 退出登录 */
    @PostMapping("/logout")
    @Auth
    public void login() {
        pignoo.writer(UserToken.class).filter(UserToken::getToken, "==", userTool.getToken()).removeAll();
    }

    @Data
    @EqualsAndHashCode(callSuper = true)
    public static class UserResult extends User {
        /** 角色 */
        private RolePojo role;
        /** 角色名称 */
        private String roleName;
    }

    /** 查询我的信息 */
    @GetMapping("/getMyInfo")
    @Auth
    public UserResult getMyInfo() {
        User user = userTool.getUser();
        UserResult result = this.mapUserToResult(user);
        result.setPassword(null);
        return result;
    }

    @Data
    public static class SetMyParam {
        /** 昵称 */
        private String nick;
        /** 电话 */
        private String phone;
    }

    /** 修改自己基本信息 */
    @Transactional
    @PostMapping("/setMy")
    @Auth
    public User setMy(@RequestBody @Validated SetMyParam param) {
        User user = pignoo.writer(User.class).filter(User::getId, "==", userTool.getUserId()).getAny();
        if (param.getNick() != null) {
            user.setNick(param.getNick());
        }
        if (param.getPhone() != null) {
            user.setPhone(param.getPhone());
        }
        return this.getMyInfo();
    }

    @Data
    public static class ResetPasswordParam {
        /** 旧密码 */
        @NotBlank
        private String oldPassword;
        /** 密码 */
        @NotBlank
        private String newPassword;
    }

    /** 重置我的密码 */
    @Transactional
    @PostMapping("/resetMyPassword")
    @Auth
    public void resetPassword(@RequestBody @Validated ResetPasswordParam param) {
        var passwordCheckI18nTip = this.passwordFormatVerification(param.getNewPassword());
        if (passwordCheckI18nTip != null) {
            throw new ApiException(ApiCode.biz_error, passwordCheckI18nTip);
        }
        User user = pignoo.writer(User.class).filter(User::getId, "==", userTool.getUserId()).getAny();
        boolean passwordOk = passwordEncoder.matches(param.getOldPassword(), user.getPassword());
        if (!passwordOk) {
            throw new ApiException(ApiCode.biz_error, I18nTips.old_password_error);
        }
        String newPasswordEncoded = passwordEncoder.encode(param.getNewPassword());
        user.setPassword(newPasswordEncoded);
    }

    @Data
    @EqualsAndHashCode(callSuper = true)
    public static class GetPageParam extends PageParam {
        /** IDs */
        private List<Long> idList;
        /** 电话 */
        private String phone;
        /** 是否启用 */
        private Boolean hasUsing;
        /** 角色ID */
        private Long roleId;
    }

    /** 分页查询后台用户 */
    @GetMapping("/getPage")
    @Auth
    public PageResult<UserResult> getPage(GetPageParam param) {
        var reader = pignoo.reader(User.class);
        if (param.getIdList() != null && param.getIdList().size() > 0) {
            reader.filter(User::getId, "in", param.getIdList());
        }
        if (StringUtil.isNotBlank(param.getPhone())) {
            reader.filter(User::getPhone, "==", param.getPhone());
        }
        if (param.getHasUsing() != null) {
            reader.filter(User::getHasUsing, "==", param.getHasUsing());
        }
        if (param.getRoleId() != null) {
            reader.filter(User::getRoleId, "==", param.getRoleId());
        }
        var userList = reader.get(param.getOffset(), param.getLimit());
        var resultList = userList.stream().map(u -> {
            UserResult r = this.mapUserToResult(u);
            r.setPassword(null);
            return r;
        }).toList();
        return new PageResult<>(reader.size(), resultList);
    }

    @Data
    public static class AddParam {
        @NotBlank
        private String code;
        @NotBlank
        private String nick;
        @NotBlank
        private String phone;
        @NotBlank
        private String username;
        @NotBlank
        private String password;
        @NotNull
        private Boolean hasUsing;
        @NotNull
        private Long roleId;
    }

    /** 添加后台用户 */
    @Transactional
    @PostMapping("/add")
    @Auth("User:Edit")
    public User add(@RequestBody @Validated AddParam param) {
        var passwordCheckI18nTip = this.passwordFormatVerification(param.getPassword());
        if (passwordCheckI18nTip != null) {
            throw new ApiException(ApiCode.biz_error, passwordCheckI18nTip);
        }
        var writer = pignoo.writer(User.class);
        User existUser = writer.copyWriter().filter(User::getUsername, "==", param.getUsername()).getAny();
        if (existUser != null) {
            throw new ApiException(ApiCode.biz_error, I18nTips.user_username_exist);
        }
        existUser = writer.copyWriter().filter(User::getCode, "==", param.getCode()).getAny();
        if (existUser != null) {
            throw new ApiException(ApiCode.biz_error, I18nTips.user_code_exist);
        }
        User user = BeanUtil.copy(param, new User());
        user.setCreateDatetime(System.currentTimeMillis());
        String passwordEncoded = passwordEncoder.encode(param.getPassword());
        user.setPassword(passwordEncoded);
        User result = writer.copyWriter().add(user);
        return writer.copyReader().filter(User::getId, "==", result.getId()).getAny();
    }

    @Data
    public static class ResetPasswordByIdParam {
        /** ID */
        @NotNull
        private Long userId;
        /** 密码 */
        @NotBlank
        private String newPassword;
    }

    /** 重置任何人密码 */
    @Transactional
    @PostMapping("/resetPasswordById")
    @Auth("User:Edit")
    public void resetPasswordById(@RequestBody @Validated ResetPasswordByIdParam param) {
        var passwordCheckI18nTip = this.passwordFormatVerification(param.getNewPassword());
        if (passwordCheckI18nTip != null) {
            throw new ApiException(ApiCode.biz_error, passwordCheckI18nTip);
        }
        User user = pignoo.writer(User.class).filter(User::getId, "==", param.getUserId()).getAny();
        if (user == null) {
            throw new ApiException(ApiCode.biz_error, I18nTips.user_not_found);
        }
        String newPasswordEncoded = passwordEncoder.encode(param.getNewPassword());
        user.setPassword(newPasswordEncoded);
    }

    @Data
    public static class SetParam {
        @NotNull
        private Long id;
        private String nick;
        private String phone;
        private Long roleId;
    }

    /** 修改用户信息 */
    @Transactional
    @PostMapping("/setById")
    @Auth("User:Edit")
    public User setById(@RequestBody @Validated SetParam param) {
        var writer = pignoo.writer(User.class).filter(User::getId, "==", param.getId());
        User user = writer.getAny();
        if (StringUtil.isNotBlank(param.getNick())) {
            user.setNick(param.getNick());
        }
        if (StringUtil.isNotBlank(param.getPhone())) {
            user.setPhone(param.getPhone());
        }
        if (param.getRoleId() != null) {
            user.setRoleId(param.getRoleId());
        }
        return writer.copyReader().getAny();
    }

    @Data
    public static class SetUsingByIdParam {
        /** ID */
        @NotNull
        private Long id;
        /** 是否启用 */
        @NotNull
        private Boolean using;
    }

    /** 停用、启用其他用户 */
    @Transactional
    @PostMapping("/setUsingById")
    @Auth("User:Edit")
    public User setUsingById(@RequestBody @Validated SetUsingByIdParam param) {
        if (userTool.getUserId().equals(param.getId())) {
            throw new ApiException(ApiCode.biz_error, I18nTips.cant_operate_yourself);
        }
        var writer = pignoo.writer(User.class).filter(User::getId, "==", param.getId());
        User user = writer.getAny();
        if (param.getUsing() != null) {
            user.setHasUsing(param.getUsing());
        }
        return writer.copyReader().getAny();
    }

    /**
     * User转UserResult
     */
    private UserResult mapUserToResult(User user) {
        UserResult result = BeanUtil.copy(user, new UserResult());
        if (user.getRoleId() != null) {
            Role role = pignoo.reader(Role.class).filter(Role::getId, "==", user.getRoleId()).getAny();
            if (role != null) {
                result.setRole(RolePojo.init(role));
                result.setRoleName(role.getName());
            }
        }
        return result;
    }

    /**
     * 密码格式验证
     */
    private I18nTips passwordFormatVerification(String password) {
        if (StringUtil.isBlank(password) || password.length() < 8 || password.length() > 20) {
            return I18nTips.password_size;
        }
        if (!password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,20}$")) {
            return I18nTips.password_format;
        }
        byte[] pwBytes = password.getBytes();
        byte x = pwBytes[0];
        byte y = pwBytes[1];
        for (int i = 2; i < pwBytes.length; i++) {
            if (x == y && y == pwBytes[i]) {
                return I18nTips.password_cant_same;
            }
            if (x + 1 == y && y + 1 == pwBytes[i]) {
                return I18nTips.password_cant_consecutive;
            }
            if (x == y + 1 && y == pwBytes[i] + 1) {
                return I18nTips.password_cant_consecutive;
            }
        }
        return null;
    }
}
